/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.gallery3d.app;

import android.os.Handler;
import android.os.Message;
import android.os.Process;

import com.android.gallery3d.common.Utils;
import com.android.gallery3d.data.ContentListener;
import com.android.gallery3d.data.MediaItem;
import com.android.gallery3d.data.MediaObject;
import com.android.gallery3d.data.MediaSet;
import com.android.gallery3d.data.Path;
import com.android.gallery3d.ui.SynchronizedHandler;

import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class AlbumSetDataLoader {
	private static final String TAG = AlbumSetDataLoader.class.getSimpleName();

	private static final int INDEX_NONE = -1;

	private static final int MIN_LOAD_COUNT = 4;

	private static final int MSG_LOAD_START = 1;
	private static final int MSG_LOAD_FINISH = 2;
	private static final int MSG_RUN_OBJECT = 3;

	public static interface DataListener {
		public void onContentChanged(int index);

		public void onSizeChanged(int size);
	}

	/**
	 * 数据加载器集合
	 */
	private final MediaSet[] mData;
	private final MediaItem[] mCoverItem;
	private final int[] mTotalCount;
	private final long[] mItemVersion;
	private final long[] mSetVersion;

	private int mActiveStart = 0;
	private int mActiveEnd = 0;

	private int mContentStart = 0;
	private int mContentEnd = 0;

	/**
	 * 数据加载器（LocalAlbumSet...）
	 */
	private final MediaSet mSource;
	private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
	private int mSize;

	private DataListener mDataListener;
	private LoadingListener mLoadingListener;
	private ReloadTask mReloadTask;

	private final Handler mMainHandler;

	private final MySourceListener mSourceListener = new MySourceListener();

	public AlbumSetDataLoader(AbstractGalleryActivity activity,
			MediaSet albumSet, int cacheSize) {
		mSource = Utils.checkNotNull(albumSet);
		mCoverItem = new MediaItem[cacheSize];
		mData = new MediaSet[cacheSize];
		mTotalCount = new int[cacheSize];
		mItemVersion = new long[cacheSize];
		mSetVersion = new long[cacheSize];
		Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
		Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);

		mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
			@Override
			public void handleMessage(Message message) {
				switch (message.what) {
				case MSG_RUN_OBJECT:
					Log.d(TAG, "Main handler -- MSG_RUN_OBJECT");
					((Runnable) message.obj).run();
					return;
				case MSG_LOAD_START:
					Log.d(TAG, "Main handler -- MSG_LOAD_START");
					if (mLoadingListener != null)
						mLoadingListener.onLoadingStarted();
					return;
				case MSG_LOAD_FINISH:
					Log.d(TAG, "Main handler -- MSG_LOAD_FINISH");
					if (mLoadingListener != null)
						mLoadingListener.onLoadingFinished(false);
					return;
				}
			}
		};
	}

	public void pause() {
		Log.d(TAG, "pause");
		mReloadTask.terminate();
		mReloadTask = null;
		mSource.removeContentListener(mSourceListener);
	}

	public void resume() {
		Log.d(TAG, "resume");
		mSource.addContentListener(mSourceListener);
		mReloadTask = new ReloadTask();
		mReloadTask.start();
	}

	private void assertIsActive(int index) {
		if (index < mActiveStart || index >= mActiveEnd) {
			throw new IllegalArgumentException(String.format(
					"%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
		}
	}

	public MediaSet getMediaSet(int index) {
		assertIsActive(index);
		return mData[index % mData.length];
	}

	public MediaItem getCoverItem(int index) {
		Log.d(TAG, "getCoverItem -- index: " + index);
		assertIsActive(index);
		return mCoverItem[index % mCoverItem.length];
	}

	public int getTotalCount(int index) {
		assertIsActive(index);
		return mTotalCount[index % mTotalCount.length];
	}

	public int getActiveStart() {
		return mActiveStart;
	}

	public boolean isActive(int index) {
		return index >= mActiveStart && index < mActiveEnd;
	}

	public int size() {
		return mSize;
	}

	// Returns the index of the MediaSet with the given path or
	// -1 if the path is not cached
	public int findSet(Path id) {
		int length = mData.length;
		for (int i = mContentStart; i < mContentEnd; i++) {
			MediaSet set = mData[i % length];
			if (set != null && id == set.getPath()) {
				return i;
			}
		}
		return -1;
	}

	private void clearSlot(int slotIndex) {
		Log.d(TAG, "clearSlot -- slotIndex: " + slotIndex);
		mData[slotIndex] = null;
		mCoverItem[slotIndex] = null;
		mTotalCount[slotIndex] = 0;
		mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
		mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
	}

	private void setContentWindow(int contentStart, int contentEnd) {
		Log.d(TAG, "setContentWindow -- contentStart: " + contentStart
				+ " contentEnd: " + contentEnd);
		if (contentStart == mContentStart && contentEnd == mContentEnd)
			return;
		int length = mCoverItem.length;

		int start = this.mContentStart;
		int end = this.mContentEnd;

		mContentStart = contentStart;
		mContentEnd = contentEnd;

		if (contentStart >= end || start >= contentEnd) {
			for (int i = start, n = end; i < n; ++i) {
				clearSlot(i % length);
			}
		} else {
			for (int i = start; i < contentStart; ++i) {
				clearSlot(i % length);
			}
			for (int i = contentEnd, n = end; i < n; ++i) {
				clearSlot(i % length);
			}
		}
		mReloadTask.notifyDirty();
	}

	public void setActiveWindow(int start, int end) {
		Log.d(TAG, "setActiveWindow -- start: " + start + " end: " + end);
		if (start == mActiveStart && end == mActiveEnd) {
			return;
		}

		Utils.assertTrue(start <= end && end - start <= mCoverItem.length
				&& end <= mSize);

		mActiveStart = start;
		mActiveEnd = end;

		int length = mCoverItem.length;
		// If no data is visible, keep the cache content
		if (start == end)
			return;

		int contentStart = Utils.clamp((start + end) / 2 - length / 2, 0,
				Math.max(0, mSize - length));
		int contentEnd = Math.min(contentStart + length, mSize);
		if (mContentStart > start || mContentEnd < end
				|| Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
			setContentWindow(contentStart, contentEnd);
		}
	}

	private class MySourceListener implements ContentListener {
		@Override
		public void onContentDirty() {
			mReloadTask.notifyDirty();
		}
	}

	public void setModelListener(DataListener listener) {
		mDataListener = listener;
	}

	public void setLoadingListener(LoadingListener listener) {
		mLoadingListener = listener;
	}

	private static class UpdateInfo {
		public long version;
		public int index;

		public int size;
		public MediaSet item;
		public MediaItem cover;
		public int totalCount;

		@Override
		public String toString() {
			StringBuffer sb = new StringBuffer();
			sb.append("UpdateInfo[ version: " + version);
			sb.append(" , index: " + index);
			sb.append(" , size: " + size);
			sb.append(" , mediaSet: " + item.toString());
			sb.append(" , mediaItem: " + cover.toString());
			sb.append(" , totalCount: " + totalCount + "]");
			return sb.toString();
		}
	}

	private class GetUpdateInfo implements Callable<UpdateInfo> {

		private final long mVersion;

		public GetUpdateInfo(long version) {
			Log.d(TAG, "GetUpdateInfo -- getUpdateInfo version: " + version);
			mVersion = version;
		}

		private int getInvalidIndex(long version) {

			long setVersion[] = mSetVersion;
			int length = setVersion.length;
			for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
				int index = i % length;
				if (setVersion[index] != version)
					return i;
			}
			return INDEX_NONE;
		}

		@Override
		public UpdateInfo call() throws Exception {
			Log.d(TAG, "GetUpdateInfo -- call.");
			int index = getInvalidIndex(mVersion);
			if (index == INDEX_NONE && mSourceVersion == mVersion) {
				return null;
			}
			UpdateInfo info = new UpdateInfo();
			info.version = mSourceVersion;
			info.index = index;
			info.size = mSize;
			return info;
		}
	}

	private class UpdateContent implements Callable<Void> {
		private final UpdateInfo mUpdateInfo;

		public UpdateContent(UpdateInfo info) {
			Log.d(TAG, "UpdateContent -- updateContent");
			mUpdateInfo = info;
		}

		@Override
		public Void call() {
			Log.d(TAG, "UpdateContent -- call");
			// Avoid notifying listeners of status change after pause
			// Otherwise gallery will be in inconsistent state after resume.
			if (mReloadTask == null) {
				return null;
			}
			UpdateInfo info = mUpdateInfo;
			mSourceVersion = info.version;
			if (mSize != info.size) {
				mSize = info.size;
				if (mDataListener != null)
					mDataListener.onSizeChanged(mSize);
				if (mContentEnd > mSize)
					mContentEnd = mSize;
				if (mActiveEnd > mSize)
					mActiveEnd = mSize;
			}
			// Note: info.index could be INDEX_NONE, i.e., -1
			if (info.index >= mContentStart && info.index < mContentEnd) {
				int pos = info.index % mCoverItem.length;
				mSetVersion[pos] = info.version;
				long itemVersion = info.item.getDataVersion();
				if (mItemVersion[pos] == itemVersion)
					return null;
				mItemVersion[pos] = itemVersion;
				mData[pos] = info.item;
				mCoverItem[pos] = info.cover;
				mTotalCount[pos] = info.totalCount;
				if (mDataListener != null && info.index >= mActiveStart
						&& info.index < mActiveEnd) {
					mDataListener.onContentChanged(info.index);
				}
			}
			return null;
		}
	}

	private <T> T executeAndWait(Callable<T> callable) {
		Log.d(TAG, "executeAndWait");
		FutureTask<T> task = new FutureTask<T>(callable);
		mMainHandler.sendMessage(mMainHandler.obtainMessage(MSG_RUN_OBJECT,
				task));
		try {
			return task.get();
		} catch (InterruptedException e) {
			return null;
		} catch (ExecutionException e) {
			throw new RuntimeException(e);
		}
	}

	// TODO: load active range first
	private class ReloadTask extends Thread {
		private volatile boolean mActive = true;
		private volatile boolean mDirty = true;
		private volatile boolean mIsLoading = false;

		private void updateLoading(boolean loading) {
			Log.d(TAG, "updateLoading -- loading: " + loading);
			if (mIsLoading == loading)
				return;
			mIsLoading = loading;
			mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START
					: MSG_LOAD_FINISH);
		}

		@Override
		public void run() {
			Log.d(TAG, "ReloadTask -- run");
			Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

			boolean updateComplete = false;
			while (mActive) {
				synchronized (this) {
					if (mActive && !mDirty && updateComplete) {
						if (!mSource.isLoading())
							updateLoading(false);
						Utils.waitWithoutInterrupt(this);
						continue;
					}
				}
				mDirty = false;
				updateLoading(true);

				long version = mSource.reload();
				UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
				updateComplete = info == null;
				if (updateComplete) {
					continue;
				}
				if (info.version != version) {
					info.version = version;
					info.size = mSource.getSubMediaSetCount();

					// If the size becomes smaller after reload(), we may
					// receive from GetUpdateInfo an index which is too
					// big. Because the main thread is not aware of the size
					// change until we call UpdateContent.
					if (info.index >= info.size) {
						info.index = INDEX_NONE;
					}
				}
				if (info.index != INDEX_NONE) {
					info.item = mSource.getSubMediaSet(info.index);
					if (info.item == null)
						continue;
					info.cover = info.item.getCoverMediaItem();
					info.totalCount = info.item.getTotalMediaItemCount();
				}
				executeAndWait(new UpdateContent(info));
			}
			updateLoading(false);
		}

		public synchronized void notifyDirty() {
			Log.d(TAG, "notifyDirty");
			mDirty = true;
			notifyAll();
		}

		public synchronized void terminate() {
			Log.d(TAG, "terminate");
			mActive = false;
			notifyAll();
		}
	}
}
